import { CanvasKeyBoardEvent, CanvasMouseEvent } from '../lib/Event'
import { EInputEventType } from './interface/EInputEventType'
import { TimerCallback } from './interface/Timer'
import { Vec2 } from './math/TSM'

// 纯数据类
// 我们不需要导出Timer类，因为只是作为内部类使用
class Timer {
  public id: number = -1; // 计时器的id号

  // 标记当前计时器是否有效，很重要的一个变量，具体看后续代码
  public enabled: boolean = false;

  public callback: TimerCallback; // 回调函数，到时间会自动调用
  public callbackData: any = undefined; // 用作回调函数的参数

  public countdown: number = 0; // 倒计时器，每次update时会倒计时
  public timeout: number = 0; //
  public onlyOnce: boolean = false;

  constructor (callback: TimerCallback) {
    this.callback = callback
  }
}
// eslint-disable-next-line no-undef
export class Application implements EventListenerObject {
    public timers: Timer[] = [];

    private _timeId: number = -1;

    private _fps: number = 0;
    /**
     * 由于Canvas是左手系，而webGL是右手系，需要FilpYCoord
     */
    public isFlipYCoord: boolean = false;

    // 我们的Application主要是canvas2D和webGL应用
    // 而canvas2D和webGL context都是从HTMLCanvasElement元素获取的
    public canvas: HTMLCanvasElement;

    // 本书中的Demo以浏览器为主
    // 我们对于mousemove事件提供一个开关变量
    // 如果下面变量设置为true，则每次鼠标移动都会触发mousemove事件
    // 否则我们就不会触发m
    public isSupportMouseMove: boolean;

    // 我们使用下面变量来标记当前鼠标是否按下状态
    // 目的是提供mousedrag事件
    protected _isMouseDown: boolean;
    protected _isRightMouseDown: boolean = false; // 为了支持鼠标按下drag事件

    // _start成员变量用于标记当前Application是否进入不间断的循环状态
    protected _start: boolean = false;
    // 由Window对象的requestAnimationFrame返回的大于0的id号
    // 我们可以使用cancelAnimationFrame ( this ._requestId )来取消动画循环
    protected _requestId: number = -1;

    // 由于计算当前更新与上一次更新之间的时间差
    // 用于基于时间的物理更新
    protected _lastTime !: number;
    protected _startTime !: number;

    /**
     * 声明每帧回调函数
     */
    public frameCallback: ((app: Application) => void) | null;

    public constructor (canvas: HTMLCanvasElement) {
      // Application基类拥有一个HTMLCanvasElement对象
      // 这样子类可以分别从该HTMLCanvasElement中获取Canvas2D或WebGL上下文对象
      this.canvas = canvas
      this._bindEvent()
      // 初始化时，mouseDown为false
      this._isMouseDown = false

      // 默认状态下，不支持mousemove事件
      this.isSupportMouseMove = false

      this.frameCallback = null

      document.oncontextmenu = function () { return false } // 禁止右键上下文菜单
    }

    /**
     * 为canvas绑定事件
     */
    private _bindEvent ():void {
      if (this.canvas === null) {
        throw new Error('在实例化应用时，没有传入canvas元素对象')
      }
      // canvas元素能够监听鼠标事件
      this.canvas.addEventListener('mousedown', this, false)
      this.canvas.addEventListener('mouseup', this, false)
      this.canvas.addEventListener('mousemove', this, false)

      // 很重要一点，键盘事件不能在canvas中触发，但是能在全局的window对象中触发
      // 因此我们能在window对象中监听键盘事件
      window.addEventListener('keydown', this, false)
      window.addEventListener('keyup', this, false)
      window.addEventListener('keypress', this, false)
    }

    /**
     * 判断当前Application是否一直在调用requestAnimationFrame
     * @returns
     */
    public isRunning (): boolean {
      return this._start
    }

    /**
     * 计算当前的FPS（Frame Per Second）
     */
    public get fps () {
      return this._fps
    }

    /**
     * 启动动画循环
     */
    public start (): void {
      if (this._start === false) {
        this._start = true
        // this . _requestId = -1 ; // 将_requestId设置为-1
        // 在start和stop函数中，_lastTime和_startTime都设置为-1
        this._lastTime = -1
        this._startTime = -1
        // 启动更新渲染循环

        this._requestId = requestAnimationFrame((msec: number): void => {
          // 启动step方法
          this.step(msec)
        })

        // 注释掉上述代码，使用下面代码来启动step方法
        // this . _requestId = requestAnimationFrame ( this . step . bind ( this ) ) ;
      }
    }

    /**
     * 不停的周而复始运动
     * @param timeStamp
     */
    protected step (timeStamp: number): void {
      // 第一次调用本函数时，设置start和lastTime为timestamp
      if (this._startTime === -1) this._startTime = timeStamp
      if (this._lastTime === -1) this._lastTime = timeStamp

      // 计算当前时间点与第一次调用step时间点的差
      const elapsedMsec = timeStamp - this._startTime

      // 计算当前时间点与上一次调用step时间点的差(可以理解为两帧之间的时间差)
      // 此时intervalSec实际是毫秒表示
      let intervalSec = (timeStamp - this._lastTime)

      // 第一帧的时候,intervalSec为0,防止0作分母
      if (intervalSec !== 0) {
        // 计算fps
        this._fps = 1000.0 / intervalSec
      }

      // 我们update使用的是秒为单位，因此转换为秒表示
      intervalSec /= 1000.0

      // 记录上一次的时间戳
      this._lastTime = timeStamp

      this._handleTimers(intervalSec)
      // console.log (" elapsedTime = " + elapsedMsec + " diffTime = " + intervalSec);
      // 先更新
      this.update(elapsedMsec, intervalSec)
      // 后渲染
      this.render()

      if (this.frameCallback !== null) {
        this.frameCallback(this)
      }
      // 递归调用，形成周而复始的前进
      requestAnimationFrame((elapsedMsec: number): void => {
        this.step(elapsedMsec)
      })
    }

    /**
     * 停止动画循环
     */
    public stop (): void {
      if (this._start) {
        // cancelAnimationFrame函数用于:
        // 取消一个先前通过调用window.requestAnimationFrame()方法添加到计划中的动画帧请求
        // alert(this._requestId);
        cancelAnimationFrame(this._requestId)
        // this . _requestId = -1 ; // 将_requestId设置为-1

        // 在start和stop函数中，_lastTime和_startTime都设置为-1
        this._lastTime = -1
        this._startTime = -1
        this._start = false
      }
    }

    /**
     * 虚方法，子类能覆写（override），用于更新
     * 注意: 第二个参数是秒为单位，第一参数是毫秒为单位
     * @param elapsedMsec
     * @param intervalSec
     */
    public update (elapsedMsec: number, intervalSec: number): void { }

    /**
     * 虚方法，子类能覆写（override），用于渲染
     */
    public render (): void { }

    /**
     * 虚函数，子类覆写（overide），用于同步各种资源后启动Application
     */
    public async run (): Promise<void> {
      // 调用start方法，该方法会启动requestAnimationFrame
      // 然后不停的进行回调
      this.start()
    }

    /**
     * 调用dispatchXXXX虚方法进行事件分发
     * handleEvent是接口EventListenerObject定义的协议分发，必须要实现
     * @param evt
     */
    public handleEvent (evt: Event): void {
      switch (evt.type) {
        case 'mousedown':
          this._isMouseDown = true
          this.onMouseDown(this._toCanvasMouseEvent(evt, EInputEventType.MOUSEDOWN))
          break
        case 'mouseup':
          this._isMouseDown = false
          this.onMouseUp(this._toCanvasMouseEvent(evt, EInputEventType.MOUSEUP))
          break
        case 'mousemove':
          // 如果isSupportMouseMove为true，才会每次鼠标移动触发mouseMove事件
          if (this.isSupportMouseMove) {
            this.onMouseMove(this._toCanvasMouseEvent(evt, EInputEventType.MOUSEMOVE))
          }
          // 同时，如果当前鼠标任意一个键处于按下状态并拖动时，触发drag事件
          if (this._isMouseDown) {
            this.onMouseDrag(this._toCanvasMouseEvent(evt, EInputEventType.MOUSEDRAG))
          }
          break
        case 'keypress':
          this.onKeyPress(this._toCanvasKeyBoardEvent(evt, EInputEventType.KEYPRESS))
          break
        case 'keydown':
          this.onKeyDown(this._toCanvasKeyBoardEvent(evt, EInputEventType.KEYDOWN))
          break
        case 'keyup':
          this.onKeyUp(this._toCanvasKeyBoardEvent(evt, EInputEventType.KEYUP))
          break
      }
    }

    protected onMouseDown (evt: CanvasMouseEvent): void {

    }

    protected onMouseUp (evt: CanvasMouseEvent): void {

    }

    protected onMouseMove (evt: CanvasMouseEvent): void {

    }

    protected onMouseDrag (evt: CanvasMouseEvent): void {

    }

    protected onKeyDown (evt: CanvasKeyBoardEvent): void {

    }

    protected onKeyUp (evt: CanvasKeyBoardEvent): void {

    }

    protected onKeyPress (evt: CanvasKeyBoardEvent): void {

    }

    /**
     * 当前canvas
     * @returns
     */
    protected getMouseCanvas (): HTMLCanvasElement {
      return this.canvas
    }

    /**
     * 将鼠标事件发生时鼠标指针的位置变换为相对当前canvas元素的偏移表示
     *
     * 这是一个私有方法，意味着只能在本类中使用,子类和其他类都无法调用本方法
     *
     * 只要是鼠标事件（down / up / move / drag .....）都需要调用本方法
     *
     * 将相对于浏览器viewport表示的点变换到相对于canvas表示的点
     * @param evt
     * @returns
     */
    private viewportToCanvasCoordinate (evt: MouseEvent): Vec2 {
      // 切记，很重要一点：
      // getBoundingClientRect方法返回的ClientRect
      const rect: DOMRect = this.getMouseCanvas().getBoundingClientRect()

      // 获取触发鼠标事件的target元素，这里总是HTMLCanvasElement
      if (evt.target) {
        const x: number = evt.clientX - rect.left
        let y: number = 0
        y = evt.clientY - rect.top // 相对于canvas左上的偏移
        if (this.isFlipYCoord) {
          y = this.getMouseCanvas().height - y
        }
        // 变成矢量表示
        const pos: Vec2 = new Vec2([x, y])
        return pos
      }

      alert('evt . target为null')
      throw new Error('evt . target为null')
    }

    /**
     * 将DOM Event对象信息转换为我们自己定义的CanvasMouseEvent事件
     * @param evt
     * @param type
     * @returns
     */
    private _toCanvasMouseEvent (evt: Event, type: EInputEventType): CanvasMouseEvent {
      // 向下转型，将Event转换为MouseEvent
      const event: MouseEvent = evt as MouseEvent
      if (type === EInputEventType.MOUSEDOWN && event.button === 2) {
        this._isRightMouseDown = true
      } else if (type === EInputEventType.MOUSEUP && event.button === 2) {
        this._isRightMouseDown = false
      }

      let buttonNum: number = event.button

      if (this._isRightMouseDown && type === EInputEventType.MOUSEDRAG) {
        buttonNum = 2
      }

      // 将客户区的鼠标pos变换到Canvas坐标系中表示
      const mousePosition: Vec2 = this.viewportToCanvasCoordinate(event)
      // 将Event一些要用到的信息传递给CanvasMouseEvent并返回
      const canvasMouseEvent: CanvasMouseEvent = new CanvasMouseEvent(type, mousePosition, buttonNum, event.altKey, event.ctrlKey, event.shiftKey)
      return canvasMouseEvent
    }

    /**
     * 将DOM Event对象信息转换为我们自己定义的Keyboard事件
     * @param evt
     * @param type
     * @returns
     */
    private _toCanvasKeyBoardEvent (evt: Event, type: EInputEventType): CanvasKeyBoardEvent {
      const event: KeyboardEvent = evt as KeyboardEvent
      // 将Event一些要用到的信息传递给CanvasKeyBoardEvent并返回
      const canvasKeyboardEvent: CanvasKeyBoardEvent = new CanvasKeyBoardEvent(type, event.key, event.keyCode, event.repeat, event.altKey, event.ctrlKey, event.shiftKey)
      return canvasKeyboardEvent
    }

    /**
     * 初始化时，timers是空列表
     *
     * 为了减少内存析构，我们在removeTimer时，并不从timers中删除掉timer，而是设置enabled为false
     *
     * 这样让内存使用量和析构达到相对平衡状态
     *
     * 每次添加一个计时器时，先查看timers列表中是否有没有时候用的timer，有的话，返回该timer的id号
     *
     * 如果没有可用的timer，就重新new一个timer，并设置其id号以及其他属性
     *
     * @param callback
     * @param timeout
     * @param onlyOnce
     * @param data
     * @returns
     */
    public addTimer (callback: TimerCallback, timeout: number = 1.0, onlyOnce: boolean = false, data: any = undefined): number {
      for (let i = 0; i < this.timers.length; i++) {
        const timer: Timer = this.timers[i]
        if (timer.enabled === false) {
          timer.callback = callback
          timer.callbackData = data
          timer.timeout = timeout
          timer.countdown = timeout
          timer.enabled = true
          timer.onlyOnce = onlyOnce
          return timer.id
        }
      }

      // 不存在，就新创建一个Timer对象
      const timer: Timer = new Timer(callback)
      timer.callbackData = data
      timer.timeout = timeout
      timer.countdown = timeout
      timer.enabled = true
      timer.id = ++this._timeId // 由于初始化时id为-1,所以前++
      timer.onlyOnce = onlyOnce // 设置是否是一次回调还是重复回调
      // 添加到timers列表中去
      this.timers.push(timer)
      // 返回新添加的timer的id号
      return timer.id
    }

    /**
     * 根据id在timers列表中查找
     *
     * 如果找到，则设置timer的enabled为false，并返回true
     *
     * 如没找到，返回false
     *
     * @param id
     * @returns
     */
    public removeTimer (id: number): boolean {
      let found: boolean = false
      for (let i = 0; i < this.timers.length; i++) {
        if (this.timers[i].id === id) {
          const timer: Timer = this.timers[i]
          timer.enabled = false // 只是enabled设置为false，并没有从数组中删除掉
          found = true
          break
        }
      }
      return found
    }

    /**
     * _handleTimers私有方法被Application的update函数调用
     *
     * update函数第二个参数是以秒表示的前后帧时间差
     *
     * 正符合_handleTimers参数要求
     *
     * 我们的计时器依赖于requestAnimationFrame回调
     *
     * 如果当前Application没有调用start的话
     *
     * 则计时器不会生效
     * @param intervalSec
     */
    private _handleTimers (intervalSec: number): void {
      // 遍历整个timers列表
      for (let i = 0; i < this.timers.length; i++) {
        const timer: Timer = this.timers[i]

        // 如果当前timer enabled为false，那么继续循环
        if (timer.enabled === false) {
          continue
        }

        // countdown初始化时 = timeout
        // 每次调用本函数，会减少上下帧的时间间隔
        // 从而形成倒计时的效果
        timer.countdown -= intervalSec

        // 如果countdown 小于 0.0，那么说明时间到了
        // 要触发回调了
        // 从这里看到，实际上timer并不是很精确的
        // 举个例子，假设我们update每次0.16秒
        // 我们的timer设置0.3秒回调一次
        // 那么实际上是 ( 0.3 - 0.32 ) < 0 ,触发回调
        if (timer.countdown < 0.0) {
          // 调用回调函数
          timer.callback(timer.id, timer.callbackData)

          // 如果该计时器需要重复触发
          if (timer.onlyOnce === false) {
            // 我们重新将countdown设置为timeout
            // 由此可见，timeout不会更改，它规定了触发的时间间隔
            // 每次更新的是countdown倒计时器
            timer.countdown = timer.timeout // 很精妙的一个技巧
          } else { // 如果该计时器只需要触发一次，那么我们就删除掉该计时器
            this.removeTimer(timer.id)
          }
        }
      }
    }
}
